﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.IO;

namespace KATS_Client.EncryptionMethods
{
    /// <summary>
    /// Rigndael Managed
    /// </summary>
    public class RijndaelM
    {
        /// <summary>
        /// The encoding to use
        /// </summary>
        static Encoding e = Encoding.Unicode;

        /// <summary>
        /// key size in bytes
        /// </summary>
        static int KeySize = 16;

        /// <summary>
        /// The encryption algorithm
        /// </summary>
        static RijndaelManaged rm;

        static RijndaelM()
        {
            rm = new RijndaelManaged();
            rm.Padding = PaddingMode.ISO10126;
            rm.KeySize =  KeySize*8;
            rm.Key = MakeKey();
            rm.IV = MakeIV(rm.IV);
        }

        /// <summary>
        /// Make the encryption key
        /// </summary>
        /// <param name="TextKey">The string to base the key off</param>
        /// <returns>The encryption key</returns>
        static byte[] MakeKey()
        {
            // convert the string into bytes
            byte[] key = ASCIIEncoding.ASCII.GetBytes(Properties.Settings.Default.EncryptionKey);

            // check the key bytes is at least the length of the keysize
            if (key.Length < KeySize)
                throw new ArgumentOutOfRangeException("Encryption Key", "The key provided is too short. Has to be " + (KeySize / 8 + 1) + " chars at least long");

            // if the key is the perfect length, return it
            if (key.Length == KeySize)
                return key;

            // if the key is too long, return the first grouping of bytes required
            byte[] newKey = new byte[KeySize];
            for (int i = 0; i < newKey.Length; i++)
            {
                newKey[i] = key[i];
            }
            return newKey;
        }

        /// <summary>
        /// Make the Initialisation vector
        /// </summary>
        /// <param name="sample">A byte array of the required size</param>
        /// <returns>The initialisation vector to be used</returns>
        static byte[] MakeIV(byte[] sample)
        {
            // Make the initialisation vector based on the final x bytes of the Key
            int size = sample.Length;


            // convert the string into bytes
            byte[] key = ASCIIEncoding.ASCII.GetBytes(Properties.Settings.Default.EncryptionKey);

            // check the key bytes is at least the length of the keysize
            if (key.Length < size)
                throw new ArgumentOutOfRangeException("Encryption Key", "The key provided is too short.");

            // if the key is the perfect length, return it
            if (key.Length == size)
                return key;

            // if the key is too long, return the final set of bytes needed
            byte[] newKey = new byte[size];
            for (int i = 0; i < size; i++)
            {
                int p = key.Length - 1 - i;
                newKey[i] = key[p];
            }

            return newKey;
        }


        /// <summary>
        /// Encrypt the string
        /// </summary>
        /// <param name="message">The string</param>
        /// <returns>The Encrypted string</returns>
        public static string Encrypt(string message)
        {
            byte[] bytes = Encrypt(e.GetBytes(message));
            return Convert.ToBase64String(bytes);
        }

        /// <summary>
        /// Encrypt the bytes
        /// </summary>
        /// <param name="bytes">The bytes to encrypt</param>
        /// <returns>Encrypted bytes</returns>
        public static byte[] Encrypt(byte[] bytes)
        {
            // create a memory stream for the cryptostream to write into
            MemoryStream ciphertextmem = new MemoryStream();

            // create a cryptostream 
            // using write as it is writing to the stream, not reading
            CryptoStream crystm = new CryptoStream(ciphertextmem, rm.CreateEncryptor(), CryptoStreamMode.Write);

            // write the bytes to the crypto stream
            crystm.Write(bytes, 0, bytes.Length);
            // close the crypto stream
            crystm.Close();

            // extract the ciphertext from the memory stream
            byte[] ciphertext = ciphertextmem.ToArray();
            // close this stream
            ciphertextmem.Close();

            // return the cipher text streams
            return ciphertext;
        }

        /// <summary>
        /// Decrypt a string
        /// </summary>
        /// <param name="message">The encoded string</param>
        /// <returns>The decrypted string</returns>
        public static string Decrypt(string message)
        {
            byte[] decrypted = Decrypt(Convert.FromBase64String(message));
            return e.GetString(decrypted);
        }

        /// <summary>
        /// Decrypt the bytes
        /// </summary>
        /// <param name="bytes">Bytes to decrypt</param>
        /// <returns>Decrypted bytes</returns>
        public static byte[] Decrypt(byte[] bytes)
        {
            // create the output memory stream
            MemoryStream ms = new MemoryStream();
            // create the decryption stream
            CryptoStream cs = new CryptoStream(ms, rm.CreateDecryptor(), CryptoStreamMode.Write);

            // write all the ciphertext into the decryption stream
            cs.Write(bytes, 0, bytes.Length);
            // close and free all resources of the decryption stream
            cs.Close();

            // get the plaintext
            byte[] decrypted = ms.ToArray();

            // return the decrypted bytes
            return decrypted;
        }


        /// <summary>
        /// Test the encryption class
        /// </summary>
        public static void Test()
        {
            Console.WriteLine("Testing Cryptology");

            List<TimeSpan> times = new List<TimeSpan>();
            int numtests = 20;

            for (int a = 0; a < numtests; a++)
            {
                DateTime start = DateTime.Now;
                int i = 320000;
                int j = 320000;
                Console.WriteLine("Checking Cryptology for byte arrays of size {0}-{1} bytes in size", i, j);

                for (; i <= j; i++)
                {
                    try
                    {
                        byte[] random = GetBytes(i);

                        byte[] encrypted = Encrypt(random);

                        byte[] decrypted = Decrypt(encrypted);

                        if (BytesEqual(random, decrypted))
                        {
                            //Console.WriteLine("Encrypt/Decrypt works");
                        }
                        else
                        {
                            //Console.WriteLine("Encrypt/Decrypt fails");
                            PrintDifferentBytes(random, decrypted);
                        }
                        //Console.Write(i + " ");
                    }
                    catch (Exception)
                    {
                        Console.WriteLine("\n" + i + " failed");
                    }
                }
                //Console.WriteLine();

                DateTime end = DateTime.Now;
                TimeSpan diff = end - start;
                times.Add(diff);
                Console.WriteLine("Time: " + diff.TotalMilliseconds);
            }

            int ms = 0;
            int max = int.MinValue;
            int min = int.MaxValue;
            foreach (TimeSpan ts in times)
            {
                max = Math.Max(max, (int)ts.TotalMilliseconds);
                min = Math.Min(min,(int) ts.TotalMilliseconds);
                ms += (int)ts.TotalMilliseconds;
            }
            int avg = ms / times.Count;


            double SDTotal = 0;
            foreach (TimeSpan ts in times)
            {
                SDTotal += Math.Pow(ts.TotalMilliseconds - avg, 2);
            }
            
            
            double SD = Math.Sqrt(SDTotal / times.Count);

            Console.WriteLine("Average : " + avg);
            Console.WriteLine("    Min : " + min);
            Console.WriteLine("    Max : " + max);
            Console.WriteLine("     SD : {0}", SD);

            //Console.WriteLine("Took {0} mins {1}.{2} seconds", diff.Minutes,diff.Seconds,diff.Milliseconds);
            Console.WriteLine("Test completed");

        }

        /// <summary>
        /// Do the byte arrays equal each other?
        /// </summary>
        /// <param name="b1">Byte[] 1</param>
        /// <param name="b2">Byte[] 2</param>
        /// <returns>True if equal</returns>
        static bool BytesEqual(byte[] b1, byte[] b2)
        {
            if (b1.Length != b2.Length)
            {
                Console.WriteLine("Diff lengths");
                return false;
            }

            for (int i = 0; i < b1.Length; i++)
            {
                if (b1[i] != b2[i])
                {
                    Console.WriteLine(b1[i] + " != " + b2[i]);
                    Console.WriteLine(i + "th byte different");
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// Print the byte positions that are different
        /// </summary>
        /// <param name="b1">Byte[] 1</param>
        /// <param name="b2">Byte[] 2</param>
        static void PrintDifferentBytes(byte[] b1, byte[] b2)
        {
            Console.WriteLine("========================");
            Console.WriteLine("Different Bytes");
            for (int i = 0; i < b1.Length; i++)
            {
                if (b1[i] != b2[i])
                {
                    Console.Write("{0} ", i);
                }
            }
            Console.WriteLine("========================");
        }

        /// <summary>
        /// Get an array of random bytes
        /// </summary>
        /// <param name="length">The size of the array</param>
        /// <returns>Array of bytes</returns>
        static byte[] GetBytes(int length)
        {
            byte[] bytes = new byte[length];
            Random r = new Random();

            r.NextBytes(bytes);

            return bytes;
        }
    }
}
